/**
 * © 2009 Susanta Tewari
 * This file is part of genomemap.
 *
 * genomemap is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * genomemap is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with genomemap.  If not, see <http://www.gnu.org/licenses/>.
 */
package genomemap.data;

import genomemap.io.DataIO;
import genomemap.util.AbstractJob;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

import genomemap.util.FreqDist;

/**
 * This singleton gives access to all genome-wide data or methods that work on them.
 * @version 1.0 12/15/2009
 * @author Susanta Tewari
 */
public class GWData extends AbstractJob {

    private static GWData instance = null;

    private FreqDist<String> repeatsFreqDist;

    private GWData.Loader loader;

    // <editor-fold defaultstate="collapsed" desc="genome.properties">
    /**
     * a properties file holding the global settings of the application
     */
    private Properties props = new Properties();

    {
        String propVal = null;
        try {
            propVal = System.getProperty("genome.properties");
            props.load(DataIO.getInstance().getPlatformIO().getInputStream(propVal));
        } catch (Exception ex) {
            throw new RuntimeException("System property: genome.properties = " + propVal);
        }
    }

    /**
     * Looks up property <code>key</code> defined in io.properties.
     * @param key the key to be looked up in io.properties
     * @return value of the key if defined in io.properties; else <code>null</code>
     *
     */
    private String getProperty(String key) {
        return props.getProperty(key);

    }

    /**
     * A property is deemed valid if it is defined and has a non-empty value in io.properties.
     * @param prop
     * @return true if the property is validated else false
     */
    private void validateProperty(String prop) {

        String value = getProperty(prop);

        if (value == null || value.length() == 0)
            throw new RuntimeException("property " + prop + "=" + value);
    }// </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="Constructor">
    private GWData() {

        validateProperty("probe.length");
        validateProperty("clone.length");

        loader = new GWData.Loader();
    }

    public static synchronized GWData getInstance() {

        if (instance == null) {
            instance = new GWData();
        }
        return instance;
    }// </editor-fold>

    public static void clearCache(){
        instance = null;
    }

    public List<Integer> getLinkageGroups() {
        return DataIO.getInstance().getLinkageGroups();
    }

    /**
     * length of the clone in bases
     * @return
     */
    public Integer getCloneLength() {
        return Integer.parseInt(getProperty("clone.length"));
    }

    /**
     * length of the probe in bases
     * @return
     */
    public Integer getProbeLength() {
        return Integer.parseInt(getProperty("probe.length"));
    }

    public Integer getRepeatCount(String clone) {
        return getRepeatsFreqDist().getFreqDist().get(clone);
    }

    public Map<Integer, Set<String>> getInverseRepeatsDist() {

        return getRepeatsFreqDist().getInverseFreqDist();
    }

    private FreqDist<String> getRepeatsFreqDist() {

        if (repeatsFreqDist == null) {

            repeatsFreqDist = new FreqDist<String>();

            for (Integer linkageGroup : getLinkageGroups())
                repeatsFreqDist.add(Chromosome.getInstance(linkageGroup).getClones());
        }

        return repeatsFreqDist;
    }

    /**
     * no caching. Loads data directly from filesystem.
     * @return
     */
    public Set<CloneSeqInfo> getCloneSeq() {
        return loader.getCloneSeq();
    }

    /**
     * no caching. Loads data directly from filesystem.
     * @return
     */
    public Set<GeneSeqInfo> getGeneSeq() {
        return loader.getGeneSeq();
    }

    public Set<String> getClones() {

        Set<CloneSeqInfo> cloneSeq = getCloneSeq();

        Set<String> result = new HashSet<String>(cloneSeq.size());

        for (CloneSeqInfo cloneSeqInfo : cloneSeq) result.add(cloneSeqInfo.cloneName);

        return result;
    }

    public Set<String> getGenes() {

        Set<GeneSeqInfo> geneSeq = getGeneSeq();

        Set<String> result = new HashSet<String>(geneSeq.size());

        for (GeneSeqInfo geneSeqInfo : geneSeq) result.add(geneSeqInfo.geneName);

        return result;
    }

    /**
     *
     * @return
     */
    public HashMap<String, Set<String>> createMasterMap() {

        // genome-wide map to store gene-clone tagging info
        HashMap<String, Set<String>> m_map = new HashMap<String, Set<String>>();

        // get clone seq info
        Set<CloneSeqInfo> clones = getCloneSeq();

        // get gene seq info
        Set<GeneSeqInfo> genes = getGeneSeq();

        // a gene tags to a clone if their region overlaps.
        // for every gene check the number of clone it overlaps and include those clones as
        // tagged for that gene
        for (GeneSeqInfo gene : genes) {

            Interval gene_interval = gene.getInterval();

            for (CloneSeqInfo clone : clones) {

                Interval clone_interval = clone.getInterval();

                if (gene_interval.overlap(clone_interval)) {

                    if (m_map.get(gene.geneName) == null) m_map.put(gene.geneName,
                                new HashSet<String>());

                    m_map.get(gene.geneName).add(clone.cloneName);
                }
            }
        }

        // add complementation data
        Map<String, Set<String>> compl_data = loader.loadComplData();

        for (String key : compl_data.keySet()) {

            if (m_map.get(key) == null) {
                m_map.put(key, new HashSet<String>());
            }

            for (String val : compl_data.get(key)) {

                if (!m_map.get(key).contains(val)) {
                    m_map.get(key).add(val);
                }

            }
        }

        return m_map;

    }

    /**
     * Map modification algorithm:
     * map --> (criterion) sorted subset --> (embed into the superset) modified map
     * 
     * The algorithm is similar to map modification algorithms in AssignmentManager.
     *
     * @param physicalMap
     * @return a new array of probes, arraanged in the ascending order of
     *         their sequence`s start position.
     */
    public List<String> getModifiedProbeMapBySeq(List<String> physicalMap) {

        // 1. get the sorted subset
        Set<CloneSeqInfo> clone_seq = getCloneSeq();

        // 1.0 create an index for frequent lookups
        Map<String, CloneSeqInfo> seq_clone_index = new HashMap<String, CloneSeqInfo>();

        for (CloneSeqInfo val : clone_seq) {

            seq_clone_index.put(val.cloneName, val);
        }

        // 1.1 sort probes based on their start position in ascending
        SortedMap<Integer, String> frag_seq_map = new TreeMap<Integer, String>();

        for (String clone : physicalMap) {

            CloneSeqInfo clone_seq_info = seq_clone_index.get(clone);

            if (clone_seq_info != null) {

                frag_seq_map.put(clone_seq_info.startPosition, clone_seq_info.cloneName);

            }
        }

        // 1.3 get the sorted subset
        Collection<String> ordered_elements = frag_seq_map.values();

        // 2 embed the sorted set into the superset

        List<String> modified_map = new ArrayList<String>();

        Iterator<String> itr = ordered_elements.iterator();

        for (int i = 0; i < physicalMap.size(); i++) {

            if (ordered_elements.contains(physicalMap.get(i))) {

                modified_map.add(itr.next());

            } else {

                modified_map.add(physicalMap.get(i));
            }

        }

        return Collections.unmodifiableList(modified_map);

    }

    public class CloneSeqInfo {

        String cloneName;

        int contigID;

        int startPosition;

        int endPosition;

        public CloneSeqInfo(String cloneName, int contigID, int startPosition, int endPosition) {
            this.cloneName = cloneName;
            this.contigID = contigID;
            this.startPosition = startPosition;
            this.endPosition = endPosition;
        }

        Interval getInterval() {

            return new Interval(startPosition, endPosition, contigID);

        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) return false;
            if (getClass() != obj.getClass()) return false;
            final CloneSeqInfo other = (CloneSeqInfo) obj;
            if ((this.cloneName == null) ? (other.cloneName != null)
                    : !this.cloneName.equals(other.cloneName)) return false;
            if (this.contigID != other.contigID) return false;
            return true;
        }

        @Override
        public int hashCode() {
            int hash = 5;
            hash = 29 * hash + (this.cloneName != null ? this.cloneName.hashCode() : 0);
            hash = 29 * hash + this.contigID;
            return hash;
        }

    }

    public class GeneSeqInfo {

        String geneName;

        String geneNCUName;

        int contigID;

        int startPosition;

        int endPosition;

        public GeneSeqInfo(String geneName, String geneNCUName, int contigID, int startPosition,
                int endPosition) {
            this.geneName = geneName;
            this.geneNCUName = geneNCUName;
            this.contigID = contigID;
            this.startPosition = startPosition;
            this.endPosition = endPosition;
        }

        Interval getInterval() {

            return new Interval(startPosition, endPosition, contigID);

        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) return false;
            if (getClass() != obj.getClass()) return false;
            final GeneSeqInfo other = (GeneSeqInfo) obj;
            if ((this.geneName == null) ? (other.geneName != null)
                    : !this.geneName.equals(other.geneName)) return false;
            return true;
        }

        @Override
        public int hashCode() {
            int hash = 7;
            hash = 11 * hash + (this.geneName != null ? this.geneName.hashCode() : 0);
            return hash;
        }

    }

    /**
     * Interval is defined for a contig id
     */
    class Interval {

        int start, end, contigID;

        public Interval(int start, int end, int contigID) {
            if (end < start) {
                throw new IllegalArgumentException(
                        "end: " + end + " should be greater than start: " + start);
            }
            this.start = start;
            this.end = end;
            this.contigID = contigID;
        }

        /**
         * Checks if the intervals overlap. The intervals overlap if they share a common
         * region for the same contig id.
         * @param that the interval to which this one attempts to overlap
         * @return
         */
        public boolean overlap(Interval that) {

            if (contigID != that.contigID) return false;

            Interval first, second;
            if (this.start < that.start) {
                first = Interval.this;
                second = that;
            } else {
                first = that;
                second = this;
            }

            return second.start <= first.end;

        }

        /**
         * Returns the length of the genome region modelled by this interval
         * @rule 5
         * @return size of the interval; must be > 0
         */
        public int size() {

            return end - start;
        }

    }

    private class Loader {

        private Set<CloneSeqInfo> cloneSeq;

        private Set<GeneSeqInfo> geneSeq;

        public Set<CloneSeqInfo> getCloneSeq() {

            if (cloneSeq == null) cloneSeq = loadCloneSeq();

            return cloneSeq;
        }

        public Set<GeneSeqInfo> getGeneSeq() {

            if (geneSeq == null) geneSeq = loadGeneSeq();

            return geneSeq;
        }

        private Set<CloneSeqInfo> loadCloneSeq() {

            Set<CloneSeqInfo> result = new HashSet<CloneSeqInfo>();

            String[][] clone_seq_data = load2DStringArray(DataIO.getInstance().
                    gw_clone_stream());

            for (String[] row : clone_seq_data) {

                result.add(new CloneSeqInfo(row[0], Integer.parseInt(row[1]), Integer.parseInt(
                        row[2]), Integer.parseInt(row[3])));
            }

            return result;
        }

        private Set<GeneSeqInfo> loadGeneSeq() {

            Set<GeneSeqInfo> result = new HashSet<GeneSeqInfo>();

            // create a map with key: ncu  and value: gene from gene_ncu data
            // this index will be used to retrieve the gene name from ncu name
            String[][] gene_ncu_data = load2DStringArray(DataIO.getInstance().
                    gw_gene_ncu_stream());

            Map<String, String> ncu_index = new HashMap<String, String>();

            for (String[] row : gene_ncu_data) {
                ncu_index.put(row[1], row[0]);
            }

            // create list of GeneSeqInfo objects for each row in ncu_data
            // get the gene name from ncu name using the ncu_index created above
            String[][] ncu_data = load2DStringArray(DataIO.getInstance().
                    gw_ncu_stream());

            for (String[] row : ncu_data) {

                result.add(new GeneSeqInfo(ncu_index.get(row[0]), row[0], Integer.parseInt(row[1]),
                        Integer.parseInt(row[2]), Integer.parseInt(row[3])));

            }

            return result;
        }

        /**
         * File Data Format:
         * actin,X148B12
         * ad-7,X128H08,X130H05
         * al-1,X107G07,X108F02,X115H03
         *
         * The input stream can be accessed by <code> AppIOInterface.getInstance().gw_compl_stream() </code>
         * @return key: gene ; value: set of cosmids
         */
        private Map<String, Set<String>> loadComplData() {
            BufferedReader reader = null;

            try {
                reader = new BufferedReader(new InputStreamReader(DataIO.getInstance().
                        gw_compl_stream()));

                String line = "";

                Map<String, Set<String>> complData =
                        new HashMap<String, Set<String>>();

                line = reader.readLine();
                while (line != null) {
                    if (line.length() > 0) {
                        String[] vals = line.split(",");
                        Set<String> cloneNames = new HashSet<String>();
                        for (int i = 1; i < vals.length; i++) {
                            cloneNames.add(vals[i]);
                        }
                        complData.put(vals[0], cloneNames);
                    }
                    line = reader.readLine();
                }
                if (complData.size() == 0) {
                    System.err.println("complmentation data: size 0");
                }

                return complData;

            } catch (IOException ex) {
                ex.printStackTrace();
                return null;
            } finally {
                try {
                    reader.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }

        }

        /**
         * By specification, all .csv format string data must have header. This method assumes a
         * header is present.
         * @param is a character inputstream to read from
         * @return a 2D string array or <code>null</code> if any inputstream related exception
         * is thrown.
         */
        private final String[][] load2DStringArray(InputStream is) {
            BufferedReader reader = null;
            try {

                // we know the line number of this file
                List<String[]> data = new ArrayList<String[]>(81);
                reader = new BufferedReader(new InputStreamReader(is));

                String line = reader.readLine();

                while ((line = reader.readLine()) != null) {

                    data.add(line.split(","));
                }

                return (String[][]) data.toArray(new String[0][]);

            } catch (IOException ex) {

                ex.printStackTrace();
                return null;

            } finally {
                try {
                    reader.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }

        }

    }
}
